<template>
  <div
    id="code"
    name="code"
    :class="{ 'http-import-code': from === 'httpImport' }"
  >
    <textarea
      ref="textarea"
      v-model="value"
      :placeholder="
        from && ['formula', 'vis-system'].includes(from)
          ? '键入$选择数据列, 键入&查看平台支持的函数'
          : ''
      "
      class="root-textarea"
    ></textarea>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
import { isFunction } from 'lodash'

// import 'codemirror/theme/yonce.css'
import 'codemirror/theme/base16-light.css'
import 'codemirror/lib/codemirror.css'

import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/dialog/dialog.css'

import 'codemirror/mode/javascript/javascript' //  mode: 'application/ld+json',
import 'codemirror/mode/sql/sql' //  mode: 'text/x-sql'
import 'codemirror/mode/python/python'
import 'codemirror/addon/edit/closebrackets'
import 'codemirror/addon/edit/matchbrackets'
import 'codemirror/addon/edit/matchtags'

import 'codemirror/addon/selection/selection-pointer'

import 'codemirror/addon/hint/show-hint'
import 'codemirror/addon/hint/anyword-hint'

import 'codemirror/addon/scroll/annotatescrollbar'
import 'codemirror/addon/search/matchesonscrollbar'
import 'codemirror/addon/search/searchcursor'
import 'codemirror/addon/search/match-highlighter'

import 'codemirror/addon/dialog/dialog'
import 'codemirror/addon/search/search'
import 'codemirror/addon/search/jump-to-line'

import 'codemirror/addon/display/placeholder'

// codemirror 折叠
import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/foldcode'
import 'codemirror/addon/fold/foldgutter'
import 'codemirror/addon/fold/brace-fold'
import 'codemirror/addon/fold/comment-fold'
// lint 相关
import 'codemirror/addon/lint/lint.css'
import 'codemirror/addon/lint/lint'
import 'codemirror/addon/lint/javascript-lint'
import 'codemirror/addon/lint/json-lint'

import 'codemirror/addon/comment/comment'
import 'codemirror/keymap/sublime'

import { stringOccurOddTimes } from '@/util/util'
import 'codemirror/addon/tern/tern.css'
import 'codemirror/addon/tern/tern'
import 'tern/plugin/complete_strings'
import 'tern/plugin/doc_comment'

// @ts-ignore
import * as CodeMirror from 'codemirror'
/* eslint-disable */
// @ts-ignore
require('script-loader!jsonlint')
// @ts-ignore
window.JSHINT = require('jshint').JSHINT
const code = require('./ecmascript.json')
const sysCode = require('./visSystemFunc.json')
// @ts-ignore
window.tern = require('tern')

@Component
export default class CodeMirrorEditor extends Vue {
  /** 编辑器模式 */
  @Prop({
    type: String,
    default() {
      return 'python'
    },
  })
  mode!: string

  /** 编辑器主题 */
  @Prop({
    type: String,
    default() {
      return 'base16-light'
    },
  })
  theme!: string

  /** 编辑器高 */
  @Prop({
    type: String,
    default() {
      return 'auto'
    },
  })
  height!: String

  //  编辑器代码 预设值
  @Prop({
    type: String,
    default() {
      return ''
    },
  })
  value!: string

  //  编辑器被使用的地方
  @Prop({
    type: String,
    default() {
      return 'others'
    },
  })
  from?: string

  //  输入$, 显示table的List
  @Prop({
    type: Array,
    default() {
      return []
    },
  })
  tableList?: Array<any>

  //  输入!, 显示table列名的List
  @Prop({
    type: Array,
    default() {
      return []
    },
  })
  tableCols?: Array<any>

  // codeMirror 实例
  public myCodeMirror: any
  //  tern server 实例
  public server: any

  /** 销毁editor实例 */
  public destroyEditor(): void {
    if (this.myCodeMirror && isFunction(this.myCodeMirror.destroy)) {
      this.myCodeMirror.destroy()
    }
  }

  //  CM 常规配置项
  public cmOption: any = {
    mode: this.mode,
    theme: this.theme,
    indentWithTabs: true,
    smartIndent: true,
    lineNumbers: true,
    gutters: [
      'CodeMirror-linenumbers',
      'CodeMirror-foldgutter',
      'CodeMirror-lint-markers',
    ],
    foldGutter: true,
    matchBrackets: true,
    autofocus: true,
    lineWrapping: true,
    tabSize: 2,
    highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true },
    hintOptions: {
      completeSingle: false,
      container: document.querySelector('.ant-modal-content') as HTMLElement,
    },
    extraKeys: {
      'Alt-F': 'findPersistent',
      Enter: (cm: any) => {
        //  仅在python mode支持，键入enter 是情况添加 \
        if (this.mode === 'python') {
          //  获取当前行code
          const lineCode = cm.getLine(cm.getCursor().line)
          //  获取光标位置
          const pos = cm.getCursor().ch
          //  光标左侧的code
          const leftString = lineCode.slice(0, pos)
          const reg = /[.*/=]/ //  正则匹配. * / =
          if (
            stringOccurOddTimes(leftString, "'") ||
            stringOccurOddTimes(leftString, '"')
          ) {
            //  光标左侧为奇数个的时候进入
            for (let i = pos; i >= 0; i -= 1) {
              //  对当前行code 从光标处，从右往左遍历
              if (lineCode[i] === "'") {
                //  单引号
                cm.replaceSelection("'") //  添加'
                cm.replaceSelection('\\') //  添加 \
                cm.replaceSelection("'", 'start') //  在下一行添加'
                break
              } else if (lineCode[i] === '"') {
                //  双引号
                cm.replaceSelection('"')
                cm.replaceSelection('\\')
                cm.replaceSelection('"', 'start')
                break
              }
            }
            //  针对  . * / = 四种情况换行时 末尾添加 \ [考虑空格的情况]
          } else if (
            reg.test(lineCode[pos - 1]) ||
            reg.test(lineCode[pos]) ||
            reg.test(lineCode[pos - 2]) ||
            reg.test(lineCode[pos + 1])
          ) {
            //  bug fix for 2 ** 3 case
            if (!(lineCode.includes('**') && lineCode.length === pos)) {
              cm.replaceSelection('\\')
            }
          }
        }
        return CodeMirror.Pass
      },
    },
    keyMap: 'sublime',
    autoCloseBrackets: true,
  }

  //  为  '$' 设置 设置hint, 显示连接上的node
  public snippets(backSpace: Boolean) {
    const that = this as any

    const curCursor: any = backSpace
      ? {
          line: that.myCodeMirror.getDoc().getCursor().line,
          ch: that.myCodeMirror.getDoc().getCursor().ch - 1,
        }
      : that.myCodeMirror.getDoc().getCursor()

    const completion: any = {
      from: curCursor,
      to: curCursor,
      list: that.tableList,
    }

    const options = {
      hint() {
        return completion
      },
    }
    that.myCodeMirror.showHint(options)

    //  当formula 使用的情况，需要处理选择的hint : c1 - test -> c1
    //  当etlSql, operator使用的情况，columnName$columnTableId -> $columnTableId
    if (
      this.from &&
      ['etlSql', 'operator', 'formula', 'vis-system'].includes(this.from)
    ) {
      let selected_item_length = 0
      //  根据选择的item_text, 进行截取，然后替换
      CodeMirror.on(completion, 'pick', function (selectedItem: any) {
        selected_item_length = selectedItem.length
        const target = that.myCodeMirror.getDoc().getCursor()
        const ch = target.ch
        const line = target.line
        const spe_index = ['formula', 'vis-system'].includes(that.from)
          ? selectedItem.indexOf('-')
          : selectedItem.lastIndexOf('$')
        that.myCodeMirror
          .getDoc()
          .replaceRange(
            ['formula', 'vis-system'].includes(that.from)
              ? selectedItem.slice(0, spe_index - 1)
              : selectedItem.slice(spe_index + 1),
            { line: line, ch: ch - selected_item_length },
            { line: line, ch: ch }
          )
      })
    }
  }

  /**
   * 为 '!' 设置hint，显示表字段
   */
  public comma() {
    const that = this as any
    const completion: any = {
      from: that.myCodeMirror.getDoc().getCursor(),
      to: that.myCodeMirror.getDoc().getCursor(),
      list: that.tableCols,
    }
    const options = {
      hint() {
        return completion
      },
    }
    that.myCodeMirror.showHint(options)

    //  仅为etlSql使用
    if (this.from && ['etlSql'].includes(this.from)) {
      let selected_item_length = 0
      //  根据选择的item_text, 进行截取，然后替换
      CodeMirror.on(completion, 'pick', function (selectedItem: any) {
        selected_item_length = selectedItem.length
        const target = that.myCodeMirror.getDoc().getCursor()
        const ch = target.ch
        const line = target.line
        that.myCodeMirror
          .getDoc()
          .replaceRange(
            selectedItem?.split(']').length > 1
              ? selectedItem?.split(']')[1]
              : selectedItem,
            { line: line, ch: ch - selected_item_length - 1 },
            { line: line, ch: ch }
          )
      })
    }
  }

  //  让父组件可以取到该实例
  public get cmInstantce() {
    return this.myCodeMirror
  }

  mounted() {
    const that = this
    this.init()
    const plugins = {
      complete_strings: true,
      doc_comment: true,
    }
    this.server = new CodeMirror.TernServer({
      defs: that.from === 'vis-system' ? [sysCode] : [code],
      // @ts-ignore
      plugins: plugins,
    })
  }

  public init() {
    const that = this as any
    this.myCodeMirror = CodeMirror.fromTextArea(
      this.$refs.textarea as HTMLTextAreaElement,
      {
        ...this.cmOption,
      }
    )

    this.myCodeMirror.on('keypress', (cm: any, change: any) => {
      //  定制来源为formula 的hint提示列表
      if (this.from && ['formula', 'vis-system'].includes(this.from)) {
        //  pre-def func for js mode
        CodeMirror.registerHelper('hintWords', 'javascript', [
          'number',
          'sum',
          'average',
          'sqrt',
          'log',
          'round',
          'abs',
          'int',
          'floor',
          'ceiling',
          'power',
          'max',
          'min',
          'aggr_max',
          'aggr_min',
          'aggr_average',
          'aggr_sum',
          'date',
          'time',
          'now',
          'timeadd',
          'string',
          'concat',
          'replace',
          'getSortBy',
        ])
      }
      //  如果是特殊字符，则不展示hint list
      if (/[\dA-Za-z]/.test(change.key)) {
        that.myCodeMirror.showHint() //  show any-word hint
      }
    })
    //  用于提示函数参数信息
    if (this.mode === 'javascript') {
      this.myCodeMirror.on('cursorActivity', (cm: any) => {
        that.server.updateArgHints(cm)
      })
    }
    //  监听$的键入，展示提示信息
    if (
      this.from &&
      ['operator', 'formula', 'etlSql', 'vis-system'].includes(this.from)
    ) {
      //  只有当自定义算子&公式的时候才有该监听
      this.myCodeMirror.on('inputRead', (cm: any, change: any) => {
        //  当input 读取到 $ 字符，hint 显示规则设置
        if (change.text[0] === '$') {
          this.snippets(false)
        }
        //当读到 ! 字符时，显示表字段名
        if (change.text[0] === '!') {
          this.comma()
        }
        if (
          ['formula', 'vis-system'].includes(that.from) &&
          /[A-Za-z_]/.test(change.text[0])
        ) {
          that.server.complete(cm)
        } else if (
          ['formula', 'vis-system'].includes(that.from) &&
          change.text[0] === '&' // 当读到 & 字符时，展示所有可用函数的列表
        ) {
          const lineCode = this.myCodeMirror.getLine(
            this.myCodeMirror.getCursor().line
          )
          if (lineCode.charAt(this.myCodeMirror.getCursor().ch - 1) != '&') {
            that.server.complete(cm)
          }
        }
      })
    }

    if (['formula', 'vis-system'].includes(that.from)) {
      this.myCodeMirror.setOption('lineNumbers', false)
      this.myCodeMirror.setOption('gutters', [])
      //  针对删除的时候，hint 提示
      this.myCodeMirror.on('keydown', (cm: any, e: any) => {
        const lineCode = cm.getLine(cm.getCursor().line)
        //  获取光标位置
        const pos = cm.getCursor().ch
        //  光标左侧的code
        const leftString = lineCode.slice(0, pos)
        const leftChar = leftString[leftString.length - 2]
        if (e.key === 'Backspace') {
          if (leftChar === '$') {
            this.snippets(true)
          } else if (leftChar && /[A-Za-z_]/.test(leftChar)) {
            that.server.complete(cm)
          } else {
            //  不显示hint list 方案
            const completion: any = {
              from: that.myCodeMirror.getDoc().getCursor(),
              to: that.myCodeMirror.getDoc().getCursor(),
              list: [], //  list为空是关键
            }
            const options = {
              hint() {
                return completion
              },
            }
            that.myCodeMirror.showHint(options)
          }
        }
      })
    }
    //  当用于formula & etlSql 时，设置背景颜色为白色
    if (this.from && ['formula', 'etlSql', 'vis-system'].includes(this.from)) {
      const cmElement = document.querySelector(
        '.CodeMirror-scroll'
      ) as HTMLTextAreaElement
      cmElement.style.backgroundColor = 'white'
    }

    if (this.height !== 'auto') {
      //  传参有height， 渲染之后设置；
      this.$nextTick(() => {
        const cmElement = document.querySelector(
          '.CodeMirror'
        ) as HTMLTextAreaElement
        cmElement.style.height = `${this.height}px`
        cmElement.style.maxHeight = `${this.height}px`
      })
    }

    //  cm 值改变的时候，触发值改动
    this.myCodeMirror.on('change', () => {
      let valueEditor = this.myCodeMirror.getValue()
      const lineCode: any = this.myCodeMirror.getLine(
        this.myCodeMirror.getCursor().line
      )
      const lastCursor: any = this.myCodeMirror.getDoc().getCursor()
      if (
        this.from &&
        ['formula', 'vis-system'].includes(this.from) &&
        lineCode.charAt(lastCursor.ch - 1) === '&'
      ) {
        const leftString = lineCode.slice(0, lastCursor.ch - 1)
        const rightString = lineCode.slice(lastCursor.ch)
        const leftSpace =
          leftString.length === 0 ||
          [' ', '+', '-', '*', '/', '^', '%', '(', ')'].includes(
            leftString.charAt(leftString.length - 1)
          )
        const rightSpace =
          rightString.length === 0 ||
          [' ', '+', '-', '*', '/', '^', '%', '(', ')'].includes(
            rightString.charAt(0)
          )
        if (leftSpace && rightSpace) {
          let leftCodeCount: number = lastCursor.ch - 1
          for (let i = 0; i < lastCursor.line; i++) {
            leftCodeCount += 1 + this.myCodeMirror.getLine(i).length
          }
          valueEditor =
            valueEditor.substring(0, leftCodeCount) +
            valueEditor.substring(leftCodeCount + 1)
          this.myCodeMirror.setValue(valueEditor)
          this.myCodeMirror
            .getDoc()
            .setCursor(lastCursor.line, lastCursor.ch - 1)
        }
      }
      this.$emit('update:value', valueEditor)
      this.$emit('change', valueEditor)
      // if (this.from === 'formula') {
      //   this.checkIfValueContainIf()
      // }
      //  先移除公式这块的格式校验，在确认之后提示错误信息
      if (this.from && ['others'].includes(this.from)) {
        //  https://jshint.com/docs/options/
        //  去除末尾分号&需要为表达式的校验
        this.myCodeMirror.setOption('lint', {
          tooltips: false,
          options: {
            asi: true,
            lastsemic: true,
            expr: true,
          },
        })
      }
    })
    //  鼠标点击，触发cell focus
    this.myCodeMirror.on('mousedown', () => {
      this.$emit('cm:click')
    })
    //  CM 聚焦
    this.myCodeMirror.on('focus', () => {
      this.$emit('cm:focus')
    })
    //  cm 失焦
    this.myCodeMirror.on('blur', () => {
      this.$emit('cm:blur')
    })

    // 销毁
    this.$once('hook:beforeDestroy', () => {
      this.destroyEditor()
    })
    this.overwriteKeyMap()
  }

  //  重写快捷键map
  public overwriteKeyMap() {
    var map = {
      'Ctrl-R': function (cm: any) {
        cm.execCommand('replace')
      },
    }
    this.myCodeMirror.addKeyMap(map)
  }

  //  检测是否值中含有if
  // public checkIfValueContainIf() {
  //   const value = this.myCodeMirror.getValue()
  //   const ele = document.querySelector('.CodeMirror')
  //   if (value.includes('if')) {
  //     //  .CodeMirror-lint-mark-error display:none
  //     ele?.classList.add('remove-error')
  //   } else {
  //     ele?.classList.remove('remove-error')
  //   }
  // }

  //  主题的变动
  @Watch('theme')
  async getThemeValue(newValue: string) {
    this.myCodeMirror.setOption('theme', newValue)
  }
  //  语言的变动
  @Watch('mode')
  getModeValue(newValue: string) {
    this.myCodeMirror.setOption('mode', newValue)
  }
  //  高度的变动
  @Watch('height')
  async getHeightValue(newValue: string) {
    const cmElement = document.querySelector(
      '.CodeMirror'
    ) as HTMLTextAreaElement
    cmElement.style.height = `${newValue}px`
    cmElement.style.maxHeight = `${newValue}px`
  }

  @Watch('value', { immediate: true })
  getValue(newValue: string) {
    if (this.myCodeMirror) {
      const valueEditor = this.myCodeMirror.getValue()
      if (newValue !== valueEditor) {
        this.myCodeMirror.setValue(newValue)
      }
    }
  }
}
</script>

<style lang="less" scoped>
/deep/ .CodeMirror {
  height: auto;
  line-height: 25px;

  //  bug fix for Horizontal scroll
  div:first-child {
    textarea {
      width: auto !important;
    }
  }
}

/deep/ .CodeMirror.remove-error .CodeMirror-lint-mark-error {
  background-image: none;
}

/deep/ .CodeMirror-focused .cm-matchhighlight {
  background-color: #e0e0e0;
  // background-image: url('');
  // background-position: bottom;
  // background-repeat: repeat-x;
}

/deep/ .CodeMirror .CodeMirror-line {
  padding: 0 11px;
}

// 提示
/deep/ .cm-keyword {
  color: #d47829 !important;
}

// 内容
/deep/ .cm-variable,
/deep/ .cm-def,
/deep/ .cm-property {
  color: #373b52 !important;
}

// /deep/ .cm-property {
//   color: #7389b1 !important;
// }

/deep/ .cm-string {
  color: #3349dd !important;
}

/deep/ .cm-comment {
  color: #14a577 !important;
}

/deep/ .CodeMirror-scroll {
  background-color: #f9f9fc;
}

/deep/ .CodeMirror-gutter-wrapper {
  left: -55px !important;
}

/deep/ .CodeMirror-vscrollbar {
  &::-webkit-scrollbar {
    width: 6px;
  }

  &::-webkit-scrollbar-thumb {
    background: rgba(144, 147, 153, 0.5);
    border-radius: 6px;
  }

  &::-webkit-scrollbar-track {
    background: transparent;
    border-radius: 5px;
  }
}

.http-import-code {
  /deep/ .CodeMirror {
    border: 1px solid #e9e9e9;
  }
  // 光标
  /deep/ .CodeMirror-cursor {
    margin-top: 10px !important;
  }

  // 左侧行数
  /deep/ .CodeMirror-gutters {
    background: #fff;
    border-right: 1px solid #e9e9e9;
  }

  /deep/ .CodeMirror-linenumber {
    color: #5d637e;
    font-size: 15px;
    font-weight: 400;
    letter-spacing: 1px;
    padding-left: 10px;
  }

  // 内容
  /deep/ .cm-variable,
  /deep/ .cm-def,
  /deep/ .cm-property {
    color: #565657 !important;
    font-size: 14px;
    letter-spacing: 1px;
  }

  /deep/ .CodeMirror-code {
    padding-bottom: 10px;
    padding-top: 10px;
  }

  /deep/ .CodeMirror-line {
    padding-left: 10px;
  }

  /deep/ .CodeMirror-scroll {
    background-color: #fff;
  }
}
</style>
<style lang="less">
.CodeMirror-lint-tooltip,
.CodeMirror-Tern-tooltip {
  z-index: 1010;
}

.CodeMirror-hints {
  z-index: 1011;
}

.CodeMirror-placeholder {
  color: rgba(34, 36, 50, 0.5) !important;
  font-size: 12px !important;
  margin-left: 8px !important;
}
</style>
